
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" lang="zh_Hans">
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>编写你的第一个 Django 应用，第 5 部分 &#8212; Django 3.2.11.dev 文档</title>
    <link rel="stylesheet" href="../_static/default.css" type="text/css" />
    <link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
    <script type="text/javascript" id="documentation_options" data-url_root="../" src="../_static/documentation_options.js"></script>
    <script type="text/javascript" src="../_static/jquery.js"></script>
    <script type="text/javascript" src="../_static/underscore.js"></script>
    <script type="text/javascript" src="../_static/doctools.js"></script>
    <script type="text/javascript" src="../_static/language_data.js"></script>
    <link rel="index" title="索引" href="../genindex.html" />
    <link rel="search" title="搜索" href="../search.html" />
    <link rel="next" title="编写你的第一个 Django 应用，第 6 部分" href="tutorial06.html" />
    <link rel="prev" title="编写你的第一个 Django 应用，第 4 部分" href="tutorial04.html" />



 
<script src="../templatebuiltins.js"></script>
<script>
(function($) {
    if (!django_template_builtins) {
       // templatebuiltins.js missing, do nothing.
       return;
    }
    $(document).ready(function() {
        // Hyperlink Django template tags and filters
        var base = "../ref/templates/builtins.html";
        if (base == "#") {
            // Special case for builtins.html itself
            base = "";
        }
        // Tags are keywords, class '.k'
        $("div.highlight\\-html\\+django span.k").each(function(i, elem) {
             var tagname = $(elem).text();
             if ($.inArray(tagname, django_template_builtins.ttags) != -1) {
                 var fragment = tagname.replace(/_/, '-');
                 $(elem).html("<a href='" + base + "#" + fragment + "'>" + tagname + "</a>");
             }
        });
        // Filters are functions, class '.nf'
        $("div.highlight\\-html\\+django span.nf").each(function(i, elem) {
             var filtername = $(elem).text();
             if ($.inArray(filtername, django_template_builtins.tfilters) != -1) {
                 var fragment = filtername.replace(/_/, '-');
                 $(elem).html("<a href='" + base + "#" + fragment + "'>" + filtername + "</a>");
             }
        });
    });
})(jQuery);(function($) {
    $(document).ready(function() {
        $(".c-tab-unix").on("click", function() {
            $("section.c-content-unix").show();
            $("section.c-content-win").hide();
            $(".c-tab-unix").prop("checked", true);
        });
        $(".c-tab-win").on("click", function() {
            $("section.c-content-win").show();
            $("section.c-content-unix").hide();
            $(".c-tab-win").prop("checked", true);
        });
    });
})(jQuery);</script>
<link rel="stylesheet" href="../_static/console-tabs.css" type="text/css" />
  </head><body>

    <div class="document">
  <div id="custom-doc" class="yui-t6">
    <div id="hd">
      <h1><a href="../index.html">Django 3.2.11.dev 文档</a></h1>
      <div id="global-nav">
        <a title="Home page" href="../index.html">Home</a>  |
        <a title="Table of contents" href="../contents.html">Table of contents</a>  |
        <a title="Global index" href="../genindex.html">Index</a>  |
        <a title="Module index" href="../py-modindex.html">Modules</a>
      </div>
      <div class="nav">
    &laquo; <a href="tutorial04.html" title="编写你的第一个 Django 应用，第 4 部分">previous</a>
     |
    <a href="index.html" title="开始" accesskey="U">up</a>
   |
    <a href="tutorial06.html" title="编写你的第一个 Django 应用，第 6 部分">next</a> &raquo;</div>
    </div>

    <div id="bd">
      <div id="yui-main">
        <div class="yui-b">
          <div class="yui-g" id="intro-tutorial05">
            
  <div class="section" id="s-writing-your-first-django-app-part-5">
<span id="writing-your-first-django-app-part-5"></span><h1>编写你的第一个 Django 应用，第 5 部分<a class="headerlink" href="#writing-your-first-django-app-part-5" title="永久链接至标题">¶</a></h1>
<p>这一篇从 <a class="reference internal" href="tutorial04.html"><span class="doc">教程第 4 部分</span></a> 结尾的地方继续讲起。我们在前几章成功的构建了一个在线投票应用，在这一部分里我们将为它创建一些自动化测试。</p>
<div class="admonition-where-to-get-help admonition">
<p class="first admonition-title">从哪里获得帮助：</p>
<p class="last">如果你在阅读本教程的过程中有任何疑问，可以前往 FAQ 的 <a class="reference internal" href="../faq/help.html"><span class="doc">获取帮助</span></a> 的版块。</p>
</div>
<div class="section" id="s-introducing-automated-testing">
<span id="introducing-automated-testing"></span><h2>自动化测试简介<a class="headerlink" href="#introducing-automated-testing" title="永久链接至标题">¶</a></h2>
<div class="section" id="s-what-are-automated-tests">
<span id="what-are-automated-tests"></span><h3>自动化测试是什么？<a class="headerlink" href="#what-are-automated-tests" title="永久链接至标题">¶</a></h3>
<p>测试代码，是用来检查你的代码能否正常运行的程序。</p>
<p>测试在不同的层次中都存在。有些测试只关注某个很小的细节（某个模型的某个方法的返回值是否满足预期？），而另一些测试可能检查对某个软件的一系列操作（<em>某一用户输入序列是否造成了预期的结果？</em>）。其实这和我们在 <a class="reference internal" href="tutorial02.html"><span class="doc">教程第 2 部分</span></a>，里做的并没有什么不同，我们使用 <a class="reference internal" href="../ref/django-admin.html#django-admin-shell"><code class="xref std std-djadmin docutils literal notranslate"><span class="pre">shell</span></code></a> 来测试某一方法的功能，或者运行某个应用并输入数据来检查它的行为。</p>
<p>真正不同的地方在于，<em>自动化</em> 测试是由某个系统帮你自动完成的。当你创建好了一系列测试，每次修改应用代码后，就可以自动检查出修改后的代码是否还像你曾经预期的那样正常工作。你不需要花费大量时间来进行手动测试。</p>
</div>
<div class="section" id="s-why-you-need-to-create-tests">
<span id="why-you-need-to-create-tests"></span><h3>为什么你需要写测试<a class="headerlink" href="#why-you-need-to-create-tests" title="永久链接至标题">¶</a></h3>
<p>但是，为什么需要测试呢？又为什么是现在呢？</p>
<p>你可能觉得学 Python/Django 对你来说已经很满足了，再学一些新东西的话看起来有点负担过重并且没什么必要。毕竟，我们的投票应用看起来已经完美工作了。写一些自动测试并不能让它工作的更好。如果写一个投票应用是你想用 Django 完成的唯一工作，那你确实没必要学写测试。但是如果你还想写更复杂的项目，现在就是学习测试写法的最好时机了。</p>
<div class="section" id="s-tests-will-save-you-time">
<span id="tests-will-save-you-time"></span><h4>测试将节约你的时间<a class="headerlink" href="#tests-will-save-you-time" title="永久链接至标题">¶</a></h4>
<p>在某种程度上，能够「判断出代码是否正常工作」的测试，就称得上是个令人满意的了。在更复杂的应用程序中，组件之间可能会有数十个复杂的交互。</p>
<p>对其中某一组件的改变，也有可能会造成意想不到的结果。判断「代码是否正常工作」意味着你需要用大量的数据来完整的测试全部代码的功能，以确保你的小修改没有对应用整体造成破坏——这太费时间了。</p>
<p>尤其是当你发现自动化测试能在几秒钟之内帮你完成这件事时，就更会觉得手动测试实在是太浪费时间了。当某人写出错误的代码时，自动化测试还能帮助你定位错误代码的位置。</p>
<p>有时候你会觉得，和富有创造性和生产力的业务代码比起来，编写枯燥的测试代码实在是太无聊了，特别是当你知道你的代码完全没有问题的时候。</p>
<p>然而，编写测试还是要比花费几个小时手动测试你的应用，或者为了找到某个小错误而胡乱翻看代码要有意义的多。</p>
</div>
<div class="section" id="s-tests-don-t-just-identify-problems-they-prevent-them">
<span id="tests-don-t-just-identify-problems-they-prevent-them"></span><h4>测试不仅能发现错误，而且能预防错误<a class="headerlink" href="#tests-don-t-just-identify-problems-they-prevent-them" title="永久链接至标题">¶</a></h4>
<p>「测试是开发的对立面」，这种思想是不对的。</p>
<p>如果没有测试，整个应用的行为意图会变得更加的不清晰。甚至当你在看自己写的代码时也是这样，有时候你需要仔细研读一段代码才能搞清楚它有什么用。</p>
<p>而测试的出现改变了这种情况。测试就好像是从内部仔细检查你的代码，当有些地方出错时，这些地方将会变得很显眼——<em>就算你自己没有意识到那里写错了</em>。</p>
</div>
<div class="section" id="s-tests-make-your-code-more-attractive">
<span id="tests-make-your-code-more-attractive"></span><h4>测试使你的代码更有吸引力<a class="headerlink" href="#tests-make-your-code-more-attractive" title="永久链接至标题">¶</a></h4>
<p>你也许遇到过这种情况：你编写了一个绝赞的软件，但是其他开发者看都不看它一眼，因为它缺少测试。没有测试的代码不值得信任。 Django 最初开发者之一的 Jacob Kaplan-Moss 说过：“项目规划时没有包含测试是不科学的。”</p>
<p>其他的开发者希望在正式使用你的代码前看到它通过了测试，这是你需要写测试的另一个重要原因。</p>
</div>
<div class="section" id="s-tests-help-teams-work-together">
<span id="tests-help-teams-work-together"></span><h4>测试有利于团队协作<a class="headerlink" href="#tests-help-teams-work-together" title="永久链接至标题">¶</a></h4>
<p>前面的几点都是从单人开发的角度来说的。复杂的应用可能由团队维护。测试的存在保证了协作者不会不小心破坏了了你的代码（也保证你不会不小心弄坏他们的）。如果你想作为一个 Django 程序员谋生的话，你必须擅长编写测试！</p>
</div>
</div>
</div>
<div class="section" id="s-basic-testing-strategies">
<span id="basic-testing-strategies"></span><h2>基础测试策略<a class="headerlink" href="#basic-testing-strategies" title="永久链接至标题">¶</a></h2>
<p>有好几种不同的方法可以写测试。</p>
<p>一些开发者遵循 &quot;<a class="reference external" href="https://en.wikipedia.org/wiki/Test-driven_development">测试驱动</a>&quot; 的开发原则，他们在写代码之前先写测试。这种方法看起来有点反直觉，但事实上，这和大多数人日常的做法是相吻合的。我们会先描述一个问题，然后写代码来解决它。「测试驱动」的开发方法只是将问题的描述抽象为了 Python 的测试样例。</p>
<p>更普遍的情况是，一个刚接触自动化测试的新手更倾向于先写代码，然后再写测试。虽然提前写测试可能更好，但是晚点写起码也比没有强。</p>
<p>有时候很难决定从哪里开始下手写测试。如果你才写了几千行 Python 代码，选择从哪里开始写测试确实不怎么简单。如果是这种情况，那么在你下次修改代码（比如加新功能，或者修复 Bug）之前写个测试是比较合理且有效的。</p>
<p>所以，我们现在就开始写吧。</p>
</div>
<div class="section" id="s-writing-our-first-test">
<span id="writing-our-first-test"></span><h2>开始写我们的第一个测试<a class="headerlink" href="#writing-our-first-test" title="永久链接至标题">¶</a></h2>
<div class="section" id="s-we-identify-a-bug">
<span id="we-identify-a-bug"></span><h3>首先得有个 Bug<a class="headerlink" href="#we-identify-a-bug" title="永久链接至标题">¶</a></h3>
<p>幸运的是，我们的 <code class="docutils literal notranslate"><span class="pre">polls</span></code> 应用现在就有一个小 bug 需要被修复：我们的要求是如果 Question 是在一天之内发布的， <code class="docutils literal notranslate"><span class="pre">Question.was_published_recently()</span></code> 方法将会返回 <code class="docutils literal notranslate"><span class="pre">True</span></code> ，然而现在这个方法在 <code class="docutils literal notranslate"><span class="pre">Question</span></code> 的 <code class="docutils literal notranslate"><span class="pre">pub_date</span></code> 字段比当前时间还晚时也会返回 True（这是个 Bug）。</p>
<p>用djadmin:<a href="#id1"><span class="problematic" id="id2">`</span></a>shell`命令确认一下这个方法的日期bug</p>
<div class="console-block" id="console-block-0">
<input class="c-tab-unix" id="c-tab-0-unix" type="radio" name="console-0" checked>
<label for="c-tab-0-unix" title="Linux/macOS">&#xf17c/&#xf179</label>
<input class="c-tab-win" id="c-tab-0-win" type="radio" name="console-0">
<label for="c-tab-0-win" title="Windows">&#xf17a</label>
<section class="c-content-unix" id="c-content-0-unix">
<div class="highlight-console notranslate"><div class="highlight"><pre><span></span><span class="gp">$ </span>python manage.py shell
</pre></div>
</div>
</section>
<section class="c-content-win" id="c-content-0-win">
<div class="highlight"><pre><span></span><span class="gp">...\&gt;</span> py manage.py shell
</pre></div>
</section>
</div>
<div class="highlight-pycon notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="kn">import</span> <span class="nn">datetime</span>
<span class="gp">&gt;&gt;&gt; </span><span class="kn">from</span> <span class="nn">django.utils</span> <span class="kn">import</span> <span class="n">timezone</span>
<span class="gp">&gt;&gt;&gt; </span><span class="kn">from</span> <span class="nn">polls.models</span> <span class="kn">import</span> <span class="n">Question</span>
<span class="gp">&gt;&gt;&gt; </span><span class="c1"># create a Question instance with pub_date 30 days in the future</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">future_question</span> <span class="o">=</span> <span class="n">Question</span><span class="p">(</span><span class="n">pub_date</span><span class="o">=</span><span class="n">timezone</span><span class="o">.</span><span class="n">now</span><span class="p">()</span> <span class="o">+</span> <span class="n">datetime</span><span class="o">.</span><span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">30</span><span class="p">))</span>
<span class="gp">&gt;&gt;&gt; </span><span class="c1"># was it published recently?</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">future_question</span><span class="o">.</span><span class="n">was_published_recently</span><span class="p">()</span>
<span class="go">True</span>
</pre></div>
</div>
<p>因为将来发生的是肯定不是最近发生的，所以代码明显是错误的。</p>
</div>
<div class="section" id="s-create-a-test-to-expose-the-bug">
<span id="create-a-test-to-expose-the-bug"></span><h3>创建一个测试来暴露这个 bug<a class="headerlink" href="#create-a-test-to-expose-the-bug" title="永久链接至标题">¶</a></h3>
<p>我们刚刚在 <a class="reference internal" href="../ref/django-admin.html#django-admin-shell"><code class="xref std std-djadmin docutils literal notranslate"><span class="pre">shell</span></code></a> 里做的测试也就是自动化测试应该做的工作。所以我们来把它改写成自动化的吧。</p>
<p>按照惯例，Django 应用的测试应该写在应用的 <code class="docutils literal notranslate"><span class="pre">tests.py</span></code> 文件里。测试系统会自动的在所有以 <code class="docutils literal notranslate"><span class="pre">tests</span></code> 开头的文件里寻找并执行测试代码。</p>
<p>将下面的代码写入 <code class="docutils literal notranslate"><span class="pre">polls</span></code> 应用里的 <code class="docutils literal notranslate"><span class="pre">tests.py</span></code> 文件内：</p>
<div class="literal-block-wrapper docutils container" id="id1">
<div class="code-block-caption"><span class="caption-text">polls/tests.py</span><a class="headerlink" href="#id1" title="永久链接至代码">¶</a></div>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">datetime</span>

<span class="kn">from</span> <span class="nn">django.test</span> <span class="kn">import</span> <span class="n">TestCase</span>
<span class="kn">from</span> <span class="nn">django.utils</span> <span class="kn">import</span> <span class="n">timezone</span>

<span class="kn">from</span> <span class="nn">.models</span> <span class="kn">import</span> <span class="n">Question</span>


<span class="k">class</span> <span class="nc">QuestionModelTests</span><span class="p">(</span><span class="n">TestCase</span><span class="p">):</span>

    <span class="k">def</span> <span class="nf">test_was_published_recently_with_future_question</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">        was_published_recently() returns False for questions whose pub_date</span>
<span class="sd">        is in the future.</span>
<span class="sd">        &quot;&quot;&quot;</span>
        <span class="n">time</span> <span class="o">=</span> <span class="n">timezone</span><span class="o">.</span><span class="n">now</span><span class="p">()</span> <span class="o">+</span> <span class="n">datetime</span><span class="o">.</span><span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span>
        <span class="n">future_question</span> <span class="o">=</span> <span class="n">Question</span><span class="p">(</span><span class="n">pub_date</span><span class="o">=</span><span class="n">time</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">assertIs</span><span class="p">(</span><span class="n">future_question</span><span class="o">.</span><span class="n">was_published_recently</span><span class="p">(),</span> <span class="kc">False</span><span class="p">)</span>
</pre></div>
</div>
</div>
<p>我们创建了一个 <a class="reference internal" href="../topics/testing/tools.html#django.test.TestCase" title="django.test.TestCase"><code class="xref py py-class docutils literal notranslate"><span class="pre">django.test.TestCase</span></code></a> 的子类，并添加了一个方法，此方法创建一个 <code class="docutils literal notranslate"><span class="pre">pub_date</span></code> 时未来某天的 <code class="docutils literal notranslate"><span class="pre">Question</span></code> 实例。然后检查它的 <code class="docutils literal notranslate"><span class="pre">was_published_recently()</span></code> 方法的返回值——它 <em>应该</em> 是 False。</p>
</div>
<div class="section" id="s-running-tests">
<span id="running-tests"></span><h3>运行测试<a class="headerlink" href="#running-tests" title="永久链接至标题">¶</a></h3>
<p>在终端中，我们通过输入以下代码运行测试:</p>
<div class="console-block" id="console-block-1">
<input class="c-tab-unix" id="c-tab-1-unix" type="radio" name="console-1" checked>
<label for="c-tab-1-unix" title="Linux/macOS">&#xf17c/&#xf179</label>
<input class="c-tab-win" id="c-tab-1-win" type="radio" name="console-1">
<label for="c-tab-1-win" title="Windows">&#xf17a</label>
<section class="c-content-unix" id="c-content-1-unix">
<div class="highlight-console notranslate"><div class="highlight"><pre><span></span><span class="gp">$ </span>python manage.py <span class="nb">test</span> polls
</pre></div>
</div>
</section>
<section class="c-content-win" id="c-content-1-win">
<div class="highlight"><pre><span></span><span class="gp">...\&gt;</span> py manage.py test polls
</pre></div>
</section>
</div>
<p>你将会看到运行结果:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">Creating</span> <span class="n">test</span> <span class="n">database</span> <span class="k">for</span> <span class="n">alias</span> <span class="s1">&#39;default&#39;</span><span class="o">...</span>
<span class="n">System</span> <span class="n">check</span> <span class="n">identified</span> <span class="n">no</span> <span class="n">issues</span> <span class="p">(</span><span class="mi">0</span> <span class="n">silenced</span><span class="p">)</span><span class="o">.</span>
<span class="n">F</span>
<span class="o">======================================================================</span>
<span class="n">FAIL</span><span class="p">:</span> <span class="n">test_was_published_recently_with_future_question</span> <span class="p">(</span><span class="n">polls</span><span class="o">.</span><span class="n">tests</span><span class="o">.</span><span class="n">QuestionModelTests</span><span class="p">)</span>
<span class="o">----------------------------------------------------------------------</span>
<span class="n">Traceback</span> <span class="p">(</span><span class="n">most</span> <span class="n">recent</span> <span class="n">call</span> <span class="n">last</span><span class="p">):</span>
  <span class="n">File</span> <span class="s2">&quot;/path/to/mysite/polls/tests.py&quot;</span><span class="p">,</span> <span class="n">line</span> <span class="mi">16</span><span class="p">,</span> <span class="ow">in</span> <span class="n">test_was_published_recently_with_future_question</span>
    <span class="bp">self</span><span class="o">.</span><span class="n">assertIs</span><span class="p">(</span><span class="n">future_question</span><span class="o">.</span><span class="n">was_published_recently</span><span class="p">(),</span> <span class="kc">False</span><span class="p">)</span>
<span class="ne">AssertionError</span><span class="p">:</span> <span class="kc">True</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">False</span>

<span class="o">----------------------------------------------------------------------</span>
<span class="n">Ran</span> <span class="mi">1</span> <span class="n">test</span> <span class="ow">in</span> <span class="mf">0.001</span><span class="n">s</span>

<span class="n">FAILED</span> <span class="p">(</span><span class="n">failures</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="n">Destroying</span> <span class="n">test</span> <span class="n">database</span> <span class="k">for</span> <span class="n">alias</span> <span class="s1">&#39;default&#39;</span><span class="o">...</span>
</pre></div>
</div>
<div class="admonition-different-error admonition">
<p class="first admonition-title">不一样的错误？</p>
<p class="last">若在此处你得到了一个 <code class="docutils literal notranslate"><span class="pre">NameError</span></code> 错误，你可能漏了 <a class="reference internal" href="tutorial02.html#tutorial02-import-timezone"><span class="std std-ref">第二步</span></a> 中将 <code class="docutils literal notranslate"><span class="pre">datetime</span></code> 和 <code class="docutils literal notranslate"><span class="pre">timezone</span></code> 导入 <code class="docutils literal notranslate"><span class="pre">polls/model.py</span></code> 的步骤。复制这些语句，然后试着重新运行测试。</p>
</div>
<p>发生了什么呢？以下是自动化测试的运行过程：</p>
<ul class="simple">
<li><code class="docutils literal notranslate"><span class="pre">python</span> <span class="pre">manage.py</span> <span class="pre">test</span> <span class="pre">polls</span></code>  将会寻找  <code class="docutils literal notranslate"><span class="pre">polls</span></code> 应用里的测试代码</li>
<li>它找到了 <a class="reference internal" href="../topics/testing/tools.html#django.test.TestCase" title="django.test.TestCase"><code class="xref py py-class docutils literal notranslate"><span class="pre">django.test.TestCase</span></code></a> 的一个子类</li>
<li>它创建一个特殊的数据库供测试使用</li>
<li>它在类中寻找测试方法——以 <code class="docutils literal notranslate"><span class="pre">test</span></code> 开头的方法。</li>
<li>在  <code class="docutils literal notranslate"><span class="pre">test_was_published_recently_with_future_question</span></code> 方法中，它创建了一个 <code class="docutils literal notranslate"><span class="pre">pub_date</span></code> 值为 30 天后的 <code class="docutils literal notranslate"><span class="pre">Question</span></code> 实例。</li>
<li>接着使用 <code class="docutils literal notranslate"><span class="pre">assertls()</span></code> 方法，发现 <code class="docutils literal notranslate"><span class="pre">was_published_recently()</span></code> 返回了 <code class="docutils literal notranslate"><span class="pre">True</span></code>，而我们期望它返回 <code class="docutils literal notranslate"><span class="pre">False</span></code>。</li>
</ul>
<p>测试系统通知我们哪些测试样例失败了，和造成测试失败的代码所在的行号。</p>
</div>
<div class="section" id="s-fixing-the-bug">
<span id="fixing-the-bug"></span><h3>修复这个 bug<a class="headerlink" href="#fixing-the-bug" title="永久链接至标题">¶</a></h3>
<p>我们早已知道，当 <code class="docutils literal notranslate"><span class="pre">pub_date</span></code> 为未来某天时， <code class="docutils literal notranslate"><span class="pre">Question.was_published_recently()</span></code> 应该返回 <code class="docutils literal notranslate"><span class="pre">False</span></code>。我们修改 <code class="docutils literal notranslate"><span class="pre">models.py</span></code> 里的方法，让它只在日期是过去式的时候才返回 <code class="docutils literal notranslate"><span class="pre">True</span></code>：</p>
<div class="literal-block-wrapper docutils container" id="id2">
<div class="code-block-caption"><span class="caption-text">polls/models.py</span><a class="headerlink" href="#id2" title="永久链接至代码">¶</a></div>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">was_published_recently</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="n">now</span> <span class="o">=</span> <span class="n">timezone</span><span class="o">.</span><span class="n">now</span><span class="p">()</span>
    <span class="k">return</span> <span class="n">now</span> <span class="o">-</span> <span class="n">datetime</span><span class="o">.</span><span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="bp">self</span><span class="o">.</span><span class="n">pub_date</span> <span class="o">&lt;=</span> <span class="n">now</span>
</pre></div>
</div>
</div>
<p>然后重新运行测试:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">Creating</span> <span class="n">test</span> <span class="n">database</span> <span class="k">for</span> <span class="n">alias</span> <span class="s1">&#39;default&#39;</span><span class="o">...</span>
<span class="n">System</span> <span class="n">check</span> <span class="n">identified</span> <span class="n">no</span> <span class="n">issues</span> <span class="p">(</span><span class="mi">0</span> <span class="n">silenced</span><span class="p">)</span><span class="o">.</span>
<span class="o">.</span>
<span class="o">----------------------------------------------------------------------</span>
<span class="n">Ran</span> <span class="mi">1</span> <span class="n">test</span> <span class="ow">in</span> <span class="mf">0.001</span><span class="n">s</span>

<span class="n">OK</span>
<span class="n">Destroying</span> <span class="n">test</span> <span class="n">database</span> <span class="k">for</span> <span class="n">alias</span> <span class="s1">&#39;default&#39;</span><span class="o">...</span>
</pre></div>
</div>
<p>发现 bug 后，我们编写了能够暴露这个 bug 的自动化测试。在修复 bug 之后，我们的代码顺利的通过了测试。</p>
<p>将来，我们的应用可能会出现其他的问题，但是我们可以肯定的是，一定不会再次出现这个 bug，因为只要运行一遍测试，就会立刻收到警告。我们可以认为应用的这一小部分代码永远是安全的。</p>
</div>
<div class="section" id="s-more-comprehensive-tests">
<span id="more-comprehensive-tests"></span><h3>更全面的测试<a class="headerlink" href="#more-comprehensive-tests" title="永久链接至标题">¶</a></h3>
<p>我们已经搞定一小部分了，现在可以考虑全面的测试 <code class="docutils literal notranslate"><span class="pre">was_published_recently()</span></code> 这个方法以确定它的安全性，然后就可以把这个方法稳定下来了。事实上，在修复一个 bug 时不小心引入另一个 bug 会是非常令人尴尬的。</p>
<p>我们在上次写的类里再增加两个测试，来更全面的测试这个方法：</p>
<div class="literal-block-wrapper docutils container" id="id3">
<div class="code-block-caption"><span class="caption-text">polls/tests.py</span><a class="headerlink" href="#id3" title="永久链接至代码">¶</a></div>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">test_was_published_recently_with_old_question</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    was_published_recently() returns False for questions whose pub_date</span>
<span class="sd">    is older than 1 day.</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="n">time</span> <span class="o">=</span> <span class="n">timezone</span><span class="o">.</span><span class="n">now</span><span class="p">()</span> <span class="o">-</span> <span class="n">datetime</span><span class="o">.</span><span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">seconds</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">old_question</span> <span class="o">=</span> <span class="n">Question</span><span class="p">(</span><span class="n">pub_date</span><span class="o">=</span><span class="n">time</span><span class="p">)</span>
    <span class="bp">self</span><span class="o">.</span><span class="n">assertIs</span><span class="p">(</span><span class="n">old_question</span><span class="o">.</span><span class="n">was_published_recently</span><span class="p">(),</span> <span class="kc">False</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">test_was_published_recently_with_recent_question</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    was_published_recently() returns True for questions whose pub_date</span>
<span class="sd">    is within the last day.</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="n">time</span> <span class="o">=</span> <span class="n">timezone</span><span class="o">.</span><span class="n">now</span><span class="p">()</span> <span class="o">-</span> <span class="n">datetime</span><span class="o">.</span><span class="n">timedelta</span><span class="p">(</span><span class="n">hours</span><span class="o">=</span><span class="mi">23</span><span class="p">,</span> <span class="n">minutes</span><span class="o">=</span><span class="mi">59</span><span class="p">,</span> <span class="n">seconds</span><span class="o">=</span><span class="mi">59</span><span class="p">)</span>
    <span class="n">recent_question</span> <span class="o">=</span> <span class="n">Question</span><span class="p">(</span><span class="n">pub_date</span><span class="o">=</span><span class="n">time</span><span class="p">)</span>
    <span class="bp">self</span><span class="o">.</span><span class="n">assertIs</span><span class="p">(</span><span class="n">recent_question</span><span class="o">.</span><span class="n">was_published_recently</span><span class="p">(),</span> <span class="kc">True</span><span class="p">)</span>
</pre></div>
</div>
</div>
<p>现在，我们有三个测试来确保 <code class="docutils literal notranslate"><span class="pre">Question.was_published_recently()</span></code> 方法对于过去，最近，和将来的三种情况都返回正确的值。</p>
<p>再次申明，尽管  <code class="docutils literal notranslate"><span class="pre">polls</span></code>  现在是个小型的应用，但是无论它以后变得到多么复杂，无论他和其他代码如何交互，我们可以在一定程度上保证我们为之编写测试的方法将按照预期的方式运行。</p>
</div>
</div>
<div class="section" id="s-test-a-view">
<span id="test-a-view"></span><h2>测试视图<a class="headerlink" href="#test-a-view" title="永久链接至标题">¶</a></h2>
<p>我们的投票应用对所有问题都一视同仁：它将会发布所有的问题，也包括那些 <code class="docutils literal notranslate"><span class="pre">pub_date</span></code> 字段值是未来的问题。我们应该改善这一点。如果 <code class="docutils literal notranslate"><span class="pre">pub_date</span></code> 设置为未来某天，这应该被解释为这个问题将在所填写的时间点才被发布，而在之前是不可见的。</p>
<div class="section" id="s-a-test-for-a-view">
<span id="a-test-for-a-view"></span><h3>针对视图的测试<a class="headerlink" href="#a-test-for-a-view" title="永久链接至标题">¶</a></h3>
<p>为了修复上述 bug ，我们这次先编写测试，然后再去改代码。事实上，这是一个「测试驱动」开发模式的实例，但其实这两者的顺序不太重要。</p>
<p>在我们的第一个测试中，我们关注代码的内部行为。我们通过模拟用户使用浏览器访问被测试的应用来检查代码行为是否符合预期。</p>
<p>在我们动手之前，先看看需要用到的工具们。</p>
</div>
<div class="section" id="s-the-django-test-client">
<span id="the-django-test-client"></span><h3>Django 测试工具之 Client<a class="headerlink" href="#the-django-test-client" title="永久链接至标题">¶</a></h3>
<p>Django 提供了一个供测试使用的 <a class="reference internal" href="../topics/testing/tools.html#django.test.Client" title="django.test.Client"><code class="xref py py-class docutils literal notranslate"><span class="pre">Client</span></code></a> 来模拟用户和视图层代码的交互。我们能在 <code class="docutils literal notranslate"><span class="pre">tests.py</span></code> 甚至是  <a class="reference internal" href="../ref/django-admin.html#django-admin-shell"><code class="xref std std-djadmin docutils literal notranslate"><span class="pre">shell</span></code></a> 中使用它。</p>
<p>我们依照惯例从  <a class="reference internal" href="../ref/django-admin.html#django-admin-shell"><code class="xref std std-djadmin docutils literal notranslate"><span class="pre">shell</span></code></a> 开始，首先我们要做一些在 <code class="docutils literal notranslate"><span class="pre">tests.py</span></code> 里不是必须的准备工作。第一步是在  <a class="reference internal" href="../ref/django-admin.html#django-admin-shell"><code class="xref std std-djadmin docutils literal notranslate"><span class="pre">shell</span></code></a> 中配置测试环境:</p>
<div class="console-block" id="console-block-2">
<input class="c-tab-unix" id="c-tab-2-unix" type="radio" name="console-2" checked>
<label for="c-tab-2-unix" title="Linux/macOS">&#xf17c/&#xf179</label>
<input class="c-tab-win" id="c-tab-2-win" type="radio" name="console-2">
<label for="c-tab-2-win" title="Windows">&#xf17a</label>
<section class="c-content-unix" id="c-content-2-unix">
<div class="highlight-console notranslate"><div class="highlight"><pre><span></span><span class="gp">$ </span>python manage.py shell
</pre></div>
</div>
</section>
<section class="c-content-win" id="c-content-2-win">
<div class="highlight"><pre><span></span><span class="gp">...\&gt;</span> py manage.py shell
</pre></div>
</section>
</div>
<div class="highlight-pycon notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="kn">from</span> <span class="nn">django.test.utils</span> <span class="kn">import</span> <span class="n">setup_test_environment</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">setup_test_environment</span><span class="p">()</span>
</pre></div>
</div>
<p><a class="reference internal" href="../topics/testing/advanced.html#django.test.utils.setup_test_environment" title="django.test.utils.setup_test_environment"><code class="xref py py-meth docutils literal notranslate"><span class="pre">setup_test_environment()</span></code></a> 提供了一个模板渲染器，允许我们为 responses 添加一些额外的属性，例如 <code class="docutils literal notranslate"><span class="pre">response.context</span></code>，未安装此 app 无法使用此功能。注意，这个方法并 <em>不会</em> 配置测试数据库，所以接下来的代码将会在当前存在的数据库上运行，输出的内容可能由于数据库内容的不同而不同。如果你的 <code class="docutils literal notranslate"><span class="pre">settings.py</span></code> 中关于 <code class="docutils literal notranslate"><span class="pre">TIME_ZONE</span></code> 的设置不对，你可能无法获取到期望的结果。如果你之前忘了设置，在继续之前检查一下。</p>
<p>然后我们需要导入 <a class="reference internal" href="../topics/testing/tools.html#django.test.TestCase" title="django.test.TestCase"><code class="xref py py-class docutils literal notranslate"><span class="pre">django.test.TestCase</span></code></a> 类（在后续 <code class="docutils literal notranslate"><span class="pre">tests.py</span></code> 的实例中我们将会使用 <a class="reference internal" href="../topics/testing/tools.html#django.test.TestCase" title="django.test.TestCase"><code class="xref py py-class docutils literal notranslate"><span class="pre">django.test.TestCase</span></code></a> 类，这个类里包含了自己的 client 实例，所以不需要这一步）:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="kn">from</span> <span class="nn">django.test</span> <span class="kn">import</span> <span class="n">Client</span>
<span class="gp">&gt;&gt;&gt; </span><span class="c1"># create an instance of the client for our use</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">client</span> <span class="o">=</span> <span class="n">Client</span><span class="p">()</span>
</pre></div>
</div>
<p>搞定了之后，我们可以要求 client 为我们工作了:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="gp">&gt;&gt;&gt; </span><span class="c1"># get a response from &#39;/&#39;</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">)</span>
<span class="go">Not Found: /</span>
<span class="gp">&gt;&gt;&gt; </span><span class="c1"># we should expect a 404 from that address; if you instead see an</span>
<span class="gp">&gt;&gt;&gt; </span><span class="c1"># &quot;Invalid HTTP_HOST header&quot; error and a 400 response, you probably</span>
<span class="gp">&gt;&gt;&gt; </span><span class="c1"># omitted the setup_test_environment() call described earlier.</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">response</span><span class="o">.</span><span class="n">status_code</span>
<span class="go">404</span>
<span class="gp">&gt;&gt;&gt; </span><span class="c1"># on the other hand we should expect to find something at &#39;/polls/&#39;</span>
<span class="gp">&gt;&gt;&gt; </span><span class="c1"># we&#39;ll use &#39;reverse()&#39; rather than a hardcoded URL</span>
<span class="gp">&gt;&gt;&gt; </span><span class="kn">from</span> <span class="nn">django.urls</span> <span class="kn">import</span> <span class="n">reverse</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">reverse</span><span class="p">(</span><span class="s1">&#39;polls:index&#39;</span><span class="p">))</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">response</span><span class="o">.</span><span class="n">status_code</span>
<span class="go">200</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">response</span><span class="o">.</span><span class="n">content</span>
<span class="go">b&#39;\n    &lt;ul&gt;\n    \n        &lt;li&gt;&lt;a href=&quot;/polls/1/&quot;&gt;What&amp;#x27;s up?&lt;/a&gt;&lt;/li&gt;\n    \n    &lt;/ul&gt;\n\n&#39;</span>
<span class="gp">&gt;&gt;&gt; </span><span class="n">response</span><span class="o">.</span><span class="n">context</span><span class="p">[</span><span class="s1">&#39;latest_question_list&#39;</span><span class="p">]</span>
<span class="go">&lt;QuerySet [&lt;Question: What&#39;s up?&gt;]&gt;</span>
</pre></div>
</div>
</div>
<div class="section" id="s-improving-our-view">
<span id="improving-our-view"></span><h3>改善视图代码<a class="headerlink" href="#improving-our-view" title="永久链接至标题">¶</a></h3>
<p>现在的投票列表会显示将来的投票（ <code class="docutils literal notranslate"><span class="pre">pub_date</span></code> 值是未来的某天)。我们来修复这个问题。</p>
<p>在 <a class="reference internal" href="tutorial04.html"><span class="doc">教程的第 4 部分</span></a>  里，我们介绍了基于 <a class="reference internal" href="../ref/class-based-views/generic-display.html#django.views.generic.list.ListView" title="django.views.generic.list.ListView"><code class="xref py py-class docutils literal notranslate"><span class="pre">ListView</span></code></a> 的视图类：</p>
<div class="literal-block-wrapper docutils container" id="id4">
<div class="code-block-caption"><span class="caption-text">polls/views.py</span><a class="headerlink" href="#id4" title="永久链接至代码">¶</a></div>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">IndexView</span><span class="p">(</span><span class="n">generic</span><span class="o">.</span><span class="n">ListView</span><span class="p">):</span>
    <span class="n">template_name</span> <span class="o">=</span> <span class="s1">&#39;polls/index.html&#39;</span>
    <span class="n">context_object_name</span> <span class="o">=</span> <span class="s1">&#39;latest_question_list&#39;</span>

    <span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="sd">&quot;&quot;&quot;Return the last five published questions.&quot;&quot;&quot;</span>
        <span class="k">return</span> <span class="n">Question</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s1">&#39;-pub_date&#39;</span><span class="p">)[:</span><span class="mi">5</span><span class="p">]</span>
</pre></div>
</div>
</div>
<p>我们需要改进  <code class="docutils literal notranslate"><span class="pre">get_queryset()</span></code> 方法，让他它能通过将 Question 的 pub_data 属性与 <code class="docutils literal notranslate"><span class="pre">timezone.now()</span></code> 相比较来判断是否应该显示此 Question。首先我们需要一行 import 语句：</p>
<div class="literal-block-wrapper docutils container" id="id5">
<div class="code-block-caption"><span class="caption-text">polls/views.py</span><a class="headerlink" href="#id5" title="永久链接至代码">¶</a></div>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.utils</span> <span class="kn">import</span> <span class="n">timezone</span>
</pre></div>
</div>
</div>
<p>然后我们把  <code class="docutils literal notranslate"><span class="pre">get_queryset</span></code>  方法改写成下面这样：</p>
<div class="literal-block-wrapper docutils container" id="id6">
<div class="code-block-caption"><span class="caption-text">polls/views.py</span><a class="headerlink" href="#id6" title="永久链接至代码">¶</a></div>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    Return the last five published questions (not including those set to be</span>
<span class="sd">    published in the future).</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="k">return</span> <span class="n">Question</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span>
        <span class="n">pub_date__lte</span><span class="o">=</span><span class="n">timezone</span><span class="o">.</span><span class="n">now</span><span class="p">()</span>
    <span class="p">)</span><span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="s1">&#39;-pub_date&#39;</span><span class="p">)[:</span><span class="mi">5</span><span class="p">]</span>
</pre></div>
</div>
</div>
<p><code class="docutils literal notranslate"><span class="pre">Question.objects.filter(pub_date__lte=timezone.now())</span></code> returns a queryset
containing <code class="docutils literal notranslate"><span class="pre">Question</span></code>s whose <code class="docutils literal notranslate"><span class="pre">pub_date</span></code> is less than or equal to - that
is, earlier than or equal to - <code class="docutils literal notranslate"><span class="pre">timezone.now</span></code>.</p>
</div>
<div class="section" id="s-testing-our-new-view">
<span id="testing-our-new-view"></span><h3>测试新视图<a class="headerlink" href="#testing-our-new-view" title="永久链接至标题">¶</a></h3>
<p>启动服务器、在浏览器中载入站点、创建一些发布时间在过去和将来的  <code class="docutils literal notranslate"><span class="pre">Questions</span></code> ，然后检验只有已经发布的  <code class="docutils literal notranslate"><span class="pre">Questions</span></code>  会展示出来，现在你可以对自己感到满意了。<em>你不想每次修改可能与这相关的代码时都重复这样做</em> —— 所以让我们基于以上 <a class="reference internal" href="../ref/django-admin.html#django-admin-shell"><code class="xref std std-djadmin docutils literal notranslate"><span class="pre">shell</span></code></a>  会话中的内容，再编写一个测试。</p>
<p>将下面的代码添加到 <code class="docutils literal notranslate"><span class="pre">polls/tests.py</span></code> ：</p>
<div class="literal-block-wrapper docutils container" id="id7">
<div class="code-block-caption"><span class="caption-text">polls/tests.py</span><a class="headerlink" href="#id7" title="永久链接至代码">¶</a></div>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.urls</span> <span class="kn">import</span> <span class="n">reverse</span>
</pre></div>
</div>
</div>
<p>然后我们写一个公用的快捷函数用于创建投票问题，再为视图创建一个测试类：</p>
<div class="literal-block-wrapper docutils container" id="id8">
<div class="code-block-caption"><span class="caption-text">polls/tests.py</span><a class="headerlink" href="#id8" title="永久链接至代码">¶</a></div>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">create_question</span><span class="p">(</span><span class="n">question_text</span><span class="p">,</span> <span class="n">days</span><span class="p">):</span>
    <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    Create a question with the given `question_text` and published the</span>
<span class="sd">    given number of `days` offset to now (negative for questions published</span>
<span class="sd">    in the past, positive for questions that have yet to be published).</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="n">time</span> <span class="o">=</span> <span class="n">timezone</span><span class="o">.</span><span class="n">now</span><span class="p">()</span> <span class="o">+</span> <span class="n">datetime</span><span class="o">.</span><span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="n">days</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">Question</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">question_text</span><span class="o">=</span><span class="n">question_text</span><span class="p">,</span> <span class="n">pub_date</span><span class="o">=</span><span class="n">time</span><span class="p">)</span>


<span class="k">class</span> <span class="nc">QuestionIndexViewTests</span><span class="p">(</span><span class="n">TestCase</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">test_no_questions</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">        If no questions exist, an appropriate message is displayed.</span>
<span class="sd">        &quot;&quot;&quot;</span>
        <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">reverse</span><span class="p">(</span><span class="s1">&#39;polls:index&#39;</span><span class="p">))</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">status_code</span><span class="p">,</span> <span class="mi">200</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">assertContains</span><span class="p">(</span><span class="n">response</span><span class="p">,</span> <span class="s2">&quot;No polls are available.&quot;</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">assertQuerysetEqual</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">context</span><span class="p">[</span><span class="s1">&#39;latest_question_list&#39;</span><span class="p">],</span> <span class="p">[])</span>

    <span class="k">def</span> <span class="nf">test_past_question</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">        Questions with a pub_date in the past are displayed on the</span>
<span class="sd">        index page.</span>
<span class="sd">        &quot;&quot;&quot;</span>
        <span class="n">question</span> <span class="o">=</span> <span class="n">create_question</span><span class="p">(</span><span class="n">question_text</span><span class="o">=</span><span class="s2">&quot;Past question.&quot;</span><span class="p">,</span> <span class="n">days</span><span class="o">=-</span><span class="mi">30</span><span class="p">)</span>
        <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">reverse</span><span class="p">(</span><span class="s1">&#39;polls:index&#39;</span><span class="p">))</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">assertQuerysetEqual</span><span class="p">(</span>
            <span class="n">response</span><span class="o">.</span><span class="n">context</span><span class="p">[</span><span class="s1">&#39;latest_question_list&#39;</span><span class="p">],</span>
            <span class="p">[</span><span class="n">question</span><span class="p">],</span>
        <span class="p">)</span>

    <span class="k">def</span> <span class="nf">test_future_question</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">        Questions with a pub_date in the future aren&#39;t displayed on</span>
<span class="sd">        the index page.</span>
<span class="sd">        &quot;&quot;&quot;</span>
        <span class="n">create_question</span><span class="p">(</span><span class="n">question_text</span><span class="o">=</span><span class="s2">&quot;Future question.&quot;</span><span class="p">,</span> <span class="n">days</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span>
        <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">reverse</span><span class="p">(</span><span class="s1">&#39;polls:index&#39;</span><span class="p">))</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">assertContains</span><span class="p">(</span><span class="n">response</span><span class="p">,</span> <span class="s2">&quot;No polls are available.&quot;</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">assertQuerysetEqual</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">context</span><span class="p">[</span><span class="s1">&#39;latest_question_list&#39;</span><span class="p">],</span> <span class="p">[])</span>

    <span class="k">def</span> <span class="nf">test_future_question_and_past_question</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">        Even if both past and future questions exist, only past questions</span>
<span class="sd">        are displayed.</span>
<span class="sd">        &quot;&quot;&quot;</span>
        <span class="n">question</span> <span class="o">=</span> <span class="n">create_question</span><span class="p">(</span><span class="n">question_text</span><span class="o">=</span><span class="s2">&quot;Past question.&quot;</span><span class="p">,</span> <span class="n">days</span><span class="o">=-</span><span class="mi">30</span><span class="p">)</span>
        <span class="n">create_question</span><span class="p">(</span><span class="n">question_text</span><span class="o">=</span><span class="s2">&quot;Future question.&quot;</span><span class="p">,</span> <span class="n">days</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span>
        <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">reverse</span><span class="p">(</span><span class="s1">&#39;polls:index&#39;</span><span class="p">))</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">assertQuerysetEqual</span><span class="p">(</span>
            <span class="n">response</span><span class="o">.</span><span class="n">context</span><span class="p">[</span><span class="s1">&#39;latest_question_list&#39;</span><span class="p">],</span>
            <span class="p">[</span><span class="n">question</span><span class="p">],</span>
        <span class="p">)</span>

    <span class="k">def</span> <span class="nf">test_two_past_questions</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">        The questions index page may display multiple questions.</span>
<span class="sd">        &quot;&quot;&quot;</span>
        <span class="n">question1</span> <span class="o">=</span> <span class="n">create_question</span><span class="p">(</span><span class="n">question_text</span><span class="o">=</span><span class="s2">&quot;Past question 1.&quot;</span><span class="p">,</span> <span class="n">days</span><span class="o">=-</span><span class="mi">30</span><span class="p">)</span>
        <span class="n">question2</span> <span class="o">=</span> <span class="n">create_question</span><span class="p">(</span><span class="n">question_text</span><span class="o">=</span><span class="s2">&quot;Past question 2.&quot;</span><span class="p">,</span> <span class="n">days</span><span class="o">=-</span><span class="mi">5</span><span class="p">)</span>
        <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">reverse</span><span class="p">(</span><span class="s1">&#39;polls:index&#39;</span><span class="p">))</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">assertQuerysetEqual</span><span class="p">(</span>
            <span class="n">response</span><span class="o">.</span><span class="n">context</span><span class="p">[</span><span class="s1">&#39;latest_question_list&#39;</span><span class="p">],</span>
            <span class="p">[</span><span class="n">question2</span><span class="p">,</span> <span class="n">question1</span><span class="p">],</span>
        <span class="p">)</span>
</pre></div>
</div>
</div>
<p>让我们更详细地看下以上这些内容。</p>
<p>首先是一个快捷函数 <code class="docutils literal notranslate"><span class="pre">create_question</span></code>，它封装了创建投票的流程，减少了重复代码。</p>
<p><code class="docutils literal notranslate"><span class="pre">test_no_questions</span></code> 方法里没有创建任何投票，它检查返回的网页上有没有 &quot;No polls are available.&quot; 这段消息和 <code class="docutils literal notranslate"><span class="pre">latest_question_list</span></code> 是否为空。注意到 <a class="reference internal" href="../topics/testing/tools.html#django.test.TestCase" title="django.test.TestCase"><code class="xref py py-class docutils literal notranslate"><span class="pre">django.test.TestCase</span></code></a> 类提供了一些额外的 assertion 方法，在这个例子中，我们使用了 <a class="reference internal" href="../topics/testing/tools.html#django.test.SimpleTestCase.assertContains" title="django.test.SimpleTestCase.assertContains"><code class="xref py py-meth docutils literal notranslate"><span class="pre">assertContains()</span></code></a> 和 <a class="reference internal" href="../topics/testing/tools.html#django.test.TransactionTestCase.assertQuerysetEqual" title="django.test.TransactionTestCase.assertQuerysetEqual"><code class="xref py py-meth docutils literal notranslate"><span class="pre">assertQuerysetEqual()</span></code></a> 。</p>
<p>在 <code class="docutils literal notranslate"><span class="pre">test_past_question</span></code> 方法中，我们创建了一个投票并检查它是否出现在列表中。</p>
<p>在 <code class="docutils literal notranslate"><span class="pre">test_future_question</span></code> 中，我们创建 <code class="docutils literal notranslate"><span class="pre">pub_date</span></code> 在未来某天的投票。数据库会在每次调用测试方法前被重置，所以第一个投票已经没了，所以主页中应该没有任何投票。</p>
<p>剩下的那些也都差不多。实际上，测试就是假装一些管理员的输入，然后通过用户端的表现是否符合预期来判断新加入的改变是否破坏了原有的系统状态。</p>
</div>
<div class="section" id="s-testing-the-detailview">
<span id="testing-the-detailview"></span><h3>测试 <code class="docutils literal notranslate"><span class="pre">DetailView</span></code><a class="headerlink" href="#testing-the-detailview" title="永久链接至标题">¶</a></h3>
<p>我们的工作似乎已经很完美了？不，还有一个问题：就算在发布日期时未来的那些投票不会在目录页 <em>index</em> 里出现，但是如果用户知道或者猜到正确的 URL ，还是可以访问到它们。所以我们得在 <code class="docutils literal notranslate"><span class="pre">DetailView</span></code> 里增加一些约束：</p>
<div class="literal-block-wrapper docutils container" id="id9">
<div class="code-block-caption"><span class="caption-text">polls/views.py</span><a class="headerlink" href="#id9" title="永久链接至代码">¶</a></div>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">DetailView</span><span class="p">(</span><span class="n">generic</span><span class="o">.</span><span class="n">DetailView</span><span class="p">):</span>
    <span class="o">...</span>
    <span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">        Excludes any questions that aren&#39;t published yet.</span>
<span class="sd">        &quot;&quot;&quot;</span>
        <span class="k">return</span> <span class="n">Question</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">pub_date__lte</span><span class="o">=</span><span class="n">timezone</span><span class="o">.</span><span class="n">now</span><span class="p">())</span>
</pre></div>
</div>
</div>
<p>然后，我们应该增加一些测试来检验 <code class="docutils literal notranslate"><span class="pre">pub_date</span></code> 在过去的 <code class="docutils literal notranslate"><span class="pre">Question</span></code> 能够被显示出来，而 <code class="docutils literal notranslate"><span class="pre">pub_date</span></code> 在未来的则不可以：</p>
<div class="literal-block-wrapper docutils container" id="id10">
<div class="code-block-caption"><span class="caption-text">polls/tests.py</span><a class="headerlink" href="#id10" title="永久链接至代码">¶</a></div>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="k">class</span> <span class="nc">QuestionDetailViewTests</span><span class="p">(</span><span class="n">TestCase</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">test_future_question</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">        The detail view of a question with a pub_date in the future</span>
<span class="sd">        returns a 404 not found.</span>
<span class="sd">        &quot;&quot;&quot;</span>
        <span class="n">future_question</span> <span class="o">=</span> <span class="n">create_question</span><span class="p">(</span><span class="n">question_text</span><span class="o">=</span><span class="s1">&#39;Future question.&#39;</span><span class="p">,</span> <span class="n">days</span><span class="o">=</span><span class="mi">5</span><span class="p">)</span>
        <span class="n">url</span> <span class="o">=</span> <span class="n">reverse</span><span class="p">(</span><span class="s1">&#39;polls:detail&#39;</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">(</span><span class="n">future_question</span><span class="o">.</span><span class="n">id</span><span class="p">,))</span>
        <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">status_code</span><span class="p">,</span> <span class="mi">404</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">test_past_question</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="sd">&quot;&quot;&quot;</span>
<span class="sd">        The detail view of a question with a pub_date in the past</span>
<span class="sd">        displays the question&#39;s text.</span>
<span class="sd">        &quot;&quot;&quot;</span>
        <span class="n">past_question</span> <span class="o">=</span> <span class="n">create_question</span><span class="p">(</span><span class="n">question_text</span><span class="o">=</span><span class="s1">&#39;Past Question.&#39;</span><span class="p">,</span> <span class="n">days</span><span class="o">=-</span><span class="mi">5</span><span class="p">)</span>
        <span class="n">url</span> <span class="o">=</span> <span class="n">reverse</span><span class="p">(</span><span class="s1">&#39;polls:detail&#39;</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">(</span><span class="n">past_question</span><span class="o">.</span><span class="n">id</span><span class="p">,))</span>
        <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">assertContains</span><span class="p">(</span><span class="n">response</span><span class="p">,</span> <span class="n">past_question</span><span class="o">.</span><span class="n">question_text</span><span class="p">)</span>
</pre></div>
</div>
</div>
</div>
<div class="section" id="s-ideas-for-more-tests">
<span id="ideas-for-more-tests"></span><h3>更多的测试思路<a class="headerlink" href="#ideas-for-more-tests" title="永久链接至标题">¶</a></h3>
<p>我们应该给 <code class="docutils literal notranslate"><span class="pre">ResultsView</span></code> 也增加一个类似的 <code class="docutils literal notranslate"><span class="pre">get_queryset</span></code> 方法，并且为它创建测试。这和我们之前干的差不多，事实上，基本就是重复一遍。</p>
<p>我们还可以从各个方面改进投票应用，但是测试会一直伴随我们。比方说，在目录页上显示一个没有选项  <code class="docutils literal notranslate"><span class="pre">Choices</span></code> 的投票问题就没什么意义。我们可以检查并排除这样的投票题。测试可以创建一个没有选项的投票，然后检查它是否被显示在目录上。当然也要创建一个有选项的投票，然后确认它确实被显示了。</p>
<p>恩，也许你想让管理员能在目录上看见未被发布的那些投票，但是普通用户看不到。不管怎么说，如果你想要增加一个新功能，那么同时一定要为它编写测试。不过你是先写代码还是先写测试那就随你了。</p>
<p>在未来的某个时刻，你一定会去查看测试代码，然后开始怀疑：「这么多的测试不会使代码越来越复杂吗？」。别着急，我们马上就会谈到这一点。</p>
</div>
</div>
<div class="section" id="s-when-testing-more-is-better">
<span id="when-testing-more-is-better"></span><h2>当需要测试的时候，测试用例越多越好<a class="headerlink" href="#when-testing-more-is-better" title="永久链接至标题">¶</a></h2>
<p>貌似我们的测试多的快要失去控制了。按照这样发展下去，测试代码就要变得比应用的实际代码还要多了。而且测试代码大多都是重复且不优雅的，特别是在和业务代码比起来的时候，这种感觉更加明显。</p>
<p><strong>但是这没关系！</strong> 就让测试代码继续肆意增长吧。大部分情况下，你写完一个测试之后就可以忘掉它了。在你继续开发的过程中，它会一直默默无闻地为你做贡献的。</p>
<p>但有时测试也需要更新。想象一下如果我们修改了视图，只显示有选项的那些投票，那么只前写的很多测试就都会失败。<em>但这也明确地告诉了我们哪些测试需要被更新</em>，所以测试也会测试自己。</p>
<p>最坏的情况是，当你继续开发的时候，发现之前的一些测试现在看来是多余的。但是这也不是什么问题，多做些测试也 <em>不错</em>。</p>
<p>如果你对测试有个整体规划，那么它们就几乎不会变得混乱。下面有几条好的建议：</p>
<ul class="simple">
<li>对于每个模型和视图都建立单独的 <code class="docutils literal notranslate"><span class="pre">TestClass</span></code></li>
<li>每个测试方法只测试一个功能</li>
<li>给每个测试方法起个能描述其功能的名字</li>
</ul>
</div>
<div class="section" id="s-further-testing">
<span id="further-testing"></span><h2>深入代码测试<a class="headerlink" href="#further-testing" title="永久链接至标题">¶</a></h2>
<p>在本教程中，我们仅仅是了解了测试的基础知识。你能做的还有很多，而且世界上有很多有用的工具来帮你完成这些有意义的事。</p>
<p>举个例子，在上述的测试中，我们已经从代码逻辑和视图响应的角度检查了应用的输出，现在你可以从一个更加 &quot;in-browser&quot; 的角度来检查最终渲染出的 HTML 是否符合预期，使用 Selenium 可以很轻松的完成这件事。这个工具不仅可以测试 Django 框架里的代码，还可以检查其他部分，比如说你的 JavaScript。它假装成是一个正在和你站点进行交互的浏览器，就好像有个真人在访问网站一样！Django 它提供了  <a class="reference internal" href="../topics/testing/tools.html#django.test.LiveServerTestCase" title="django.test.LiveServerTestCase"><code class="xref py py-class docutils literal notranslate"><span class="pre">LiveServerTestCase</span></code></a>  来和 Selenium 这样的工具进行交互。</p>
<p>如果你在开发一个很复杂的应用的话，你也许想在每次提交代码时自动运行测试，也就是我们所说的持续集成  <a class="reference external" href="https://en.wikipedia.org/wiki/Continuous_integration">continuous integration</a> ，这样就能实现质量控制的自动化，起码是部分自动化。</p>
<p>一个找出代码中未被测试部分的方法是检查代码覆盖率。它有助于找出代码中的薄弱部分和无用部分。如果你无法测试一段代码，通常说明这段代码需要被重构或者删除。想知道代码覆盖率和无用代码的详细信息，查看文档 <a class="reference internal" href="../topics/testing/advanced.html#topics-testing-code-coverage"><span class="std std-ref">集成 coverage.py</span></a> 获取详细信息。</p>
<p>文档 <a class="reference internal" href="../topics/testing/index.html"><span class="doc">Django 中的测试</span></a> 里有关于测试的更多信息。</p>
</div>
<div class="section" id="s-what-s-next">
<span id="what-s-next"></span><h2>接下来要做什么？<a class="headerlink" href="#what-s-next" title="永久链接至标题">¶</a></h2>
<p>如果你想深入了解测试，就去看 <a class="reference internal" href="../topics/testing/index.html"><span class="doc">Django 中的测试</span></a> 。</p>
<p>当你已经比较熟悉测试 Django 视图的方法后，就可以继续阅读 <a class="reference internal" href="tutorial06.html"><span class="doc">教程第 6 部分</span></a>   ，学习静态文件管理的相关知识。</p>
</div>
</div>


          </div>
        </div>
      </div>
      
        
          <div class="yui-b" id="sidebar">
            
      <div class="sphinxsidebar" role="navigation" aria-label="main navigation">
        <div class="sphinxsidebarwrapper">
  <h3><a href="../contents.html">Table of Contents</a></h3>
  <ul>
<li><a class="reference internal" href="#">编写你的第一个 Django 应用，第 5 部分</a><ul>
<li><a class="reference internal" href="#introducing-automated-testing">自动化测试简介</a><ul>
<li><a class="reference internal" href="#what-are-automated-tests">自动化测试是什么？</a></li>
<li><a class="reference internal" href="#why-you-need-to-create-tests">为什么你需要写测试</a><ul>
<li><a class="reference internal" href="#tests-will-save-you-time">测试将节约你的时间</a></li>
<li><a class="reference internal" href="#tests-don-t-just-identify-problems-they-prevent-them">测试不仅能发现错误，而且能预防错误</a></li>
<li><a class="reference internal" href="#tests-make-your-code-more-attractive">测试使你的代码更有吸引力</a></li>
<li><a class="reference internal" href="#tests-help-teams-work-together">测试有利于团队协作</a></li>
</ul>
</li>
</ul>
</li>
<li><a class="reference internal" href="#basic-testing-strategies">基础测试策略</a></li>
<li><a class="reference internal" href="#writing-our-first-test">开始写我们的第一个测试</a><ul>
<li><a class="reference internal" href="#we-identify-a-bug">首先得有个 Bug</a></li>
<li><a class="reference internal" href="#create-a-test-to-expose-the-bug">创建一个测试来暴露这个 bug</a></li>
<li><a class="reference internal" href="#running-tests">运行测试</a></li>
<li><a class="reference internal" href="#fixing-the-bug">修复这个 bug</a></li>
<li><a class="reference internal" href="#more-comprehensive-tests">更全面的测试</a></li>
</ul>
</li>
<li><a class="reference internal" href="#test-a-view">测试视图</a><ul>
<li><a class="reference internal" href="#a-test-for-a-view">针对视图的测试</a></li>
<li><a class="reference internal" href="#the-django-test-client">Django 测试工具之 Client</a></li>
<li><a class="reference internal" href="#improving-our-view">改善视图代码</a></li>
<li><a class="reference internal" href="#testing-our-new-view">测试新视图</a></li>
<li><a class="reference internal" href="#testing-the-detailview">测试 <code class="docutils literal notranslate"><span class="pre">DetailView</span></code></a></li>
<li><a class="reference internal" href="#ideas-for-more-tests">更多的测试思路</a></li>
</ul>
</li>
<li><a class="reference internal" href="#when-testing-more-is-better">当需要测试的时候，测试用例越多越好</a></li>
<li><a class="reference internal" href="#further-testing">深入代码测试</a></li>
<li><a class="reference internal" href="#what-s-next">接下来要做什么？</a></li>
</ul>
</li>
</ul>

  <h4>上一个主题</h4>
  <p class="topless"><a href="tutorial04.html"
                        title="上一章">编写你的第一个 Django 应用，第 4 部分</a></p>
  <h4>下一个主题</h4>
  <p class="topless"><a href="tutorial06.html"
                        title="下一章">编写你的第一个 Django 应用，第 6 部分</a></p>
  <div role="note" aria-label="source link">
    <h3>本页</h3>
    <ul class="this-page-menu">
      <li><a href="../_sources/intro/tutorial05.txt"
            rel="nofollow">显示源代码</a></li>
    </ul>
   </div>
<div id="searchbox" style="display: none" role="search">
  <h3>快速搜索</h3>
    <div class="searchformwrapper">
    <form class="search" action="../search.html" method="get">
      <input type="text" name="q" />
      <input type="submit" value="转向" />
      <input type="hidden" name="check_keywords" value="yes" />
      <input type="hidden" name="area" value="default" />
    </form>
    </div>
</div>
<script type="text/javascript">$('#searchbox').show(0);</script>
        </div>
      </div>
              <h3>Last update:</h3>
              <p class="topless">12月 07, 2021</p>
          </div>
        
      
    </div>

    <div id="ft">
      <div class="nav">
    &laquo; <a href="tutorial04.html" title="编写你的第一个 Django 应用，第 4 部分">previous</a>
     |
    <a href="index.html" title="开始" accesskey="U">up</a>
   |
    <a href="tutorial06.html" title="编写你的第一个 Django 应用，第 6 部分">next</a> &raquo;</div>
    </div>
  </div>

      <div class="clearer"></div>
    </div>
  </body>
</html>